﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Configuration.Install;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Security;
using System.ServiceProcess;
using DeSleeper.Library;
using DeSleeper.Library.IO;
using DeSleeper.Library.Tasks;

namespace DeSleeper.Client.Installation
{
    internal class Installer
    {
// ReSharper disable ConvertToConstant
        private static string _rCtrlInstallUtilCmdLine = "Microsoft.NET\\Framework\\v2.0.50727\\InstallUtil";
        private static string _psExecInstallUtilCmdLine = "%windir%\\Microsoft.NET\\Framework\\v2.0.50727\\InstallUtil";
// ReSharper restore ConvertToConstant
        public const string ServiceExecutableName = "DeSleeper.ServiceHost.exe";
        public const string LocalServiceFileLocation = @"C:\Program Files\deSleeper";
        private const string _remoteServiceFileLocationTemplate = @"\\{0}\C$\Program Files\deSleeper";
        private const string ServiceName = "WakeOnLanProxyService";

        public static string GetRemoteServiceFileLocation(string machineName)
        {
            return string.Format(_remoteServiceFileLocationTemplate, machineName);
        }

        public void InstallService(InstallServiceOptions options)
        {
            var state = new Hashtable();
            var parameters = options.GetArguments();

            using (var installer = new AssemblyInstaller(ServiceExecutableName, parameters.ToArray()) { UseNewContext = true })
            {
                installer.Install(state);
                installer.Commit(state);
            }
        }

        public void InstallService(string machineName, InstallServiceOptions options)
        {
            var installStartInfo = GetInstallInfo(options);
            installStartInfo.CreateNoWindow = true;
            var installProcess = RemoteProcess.Start(installStartInfo, machineName);
            installProcess.WaitForExit();
            installProcess.ThrowOn(exitCode => exitCode != 0);
        }

        public void UninstallService()
        {
            var state = new Hashtable();
            using (var installer = new AssemblyInstaller(ServiceExecutableName, null) { UseNewContext = true })
            {
                installer.Uninstall(state);
            }
        }

        public void UninstallService(string machineName)
        {
            var uninstallStartInfo = GetUninstallInfo();
            uninstallStartInfo.CreateNoWindow = true;
            var unInstallProcess = RemoteProcess.Start(uninstallStartInfo, machineName);
            unInstallProcess.WaitForExit();
            unInstallProcess.ThrowOn(exitCode => exitCode != 0);
        }

        public ProcessStartInfo GetInstallInfo(InstallServiceOptions options)
        {
            var arguments = options.GetArguments();
            // We do not attempt to look up windir because this command line is going to be executed on another system.
            return new ProcessStartInfo
            {
                FileName = _rCtrlInstallUtilCmdLine,
                Arguments = string.Format("{0} {1}",
                                          ServiceExecutableName,
                                          string.Join(" ", arguments.ToArray())),
                WorkingDirectory = LocalServiceFileLocation,
                RedirectStandardError = true,
                RedirectStandardOutput = true
            };
        }

        public ProcessStartInfo GetUninstallInfo()
        {
            // We do not attempt to look up windir because this command line is going to be executed on another system.
            return new ProcessStartInfo
            {
                FileName = _rCtrlInstallUtilCmdLine,
                Arguments = string.Format("/u {0}", ServiceExecutableName),
                WorkingDirectory = LocalServiceFileLocation,
                RedirectStandardError = true,
                RedirectStandardOutput = true
            };
        }
        
        /// <summary>
        /// Uses the remote registry service to check if a remote machine has .NET installed.
        /// </summary>
        /// <param name="machineName">The host name of the machine to check.</param>
        /// <returns>True if installed, false otherwise.</returns>
        /// <exception cref="UnauthorizedAccessException">Remote registry service access is required to check if .NET is installed.</exception>
        /// <exception cref="IOException">Remote registry service access is required to check if .NET is installed.</exception>
        /// <exception cref="SecurityException">Remote registry service access is required to check if .NET is installed.</exception>
        public static bool CheckDotNetInstalled(string machineName)
        {
            try
            {
                var dotNetVersion = DotNetVersion.GetVersion(machineName);
                if (dotNetVersion == null || dotNetVersion.CompleteVersion < new Version(3, 5, 30729, 1))
                    return false;

                return true;
            }
            catch (UnauthorizedAccessException exception)
            {
                throw new UnauthorizedAccessException("Remote registry service access is required to check if .NET is installed.", exception);
            }
            catch(IOException exception)
            {
                throw new IOException("Remote registry service access is required to check if .NET is installed.", exception);
            }
            catch (SecurityException exception)
            {
                throw new SecurityException("Remote registry service access is required to check if .NET is installed.", exception);
            }
        }

        public void StageServiceFiles(string machineName)
        {
            try
            {
                var destination = new DirectoryInfo(GetRemoteServiceFileLocation(machineName));
                if (!destination.Exists)
                    destination.Create();

                var filesToCopy = new List<FileInfo>
                                      {
                                          new FileInfo(ServiceExecutableName),
                                          new FileInfo("XPowerCfg.exe"),
                                          new FileInfo("DeSleeper.Library.dll"),
                                          new FileInfo("DeSleeper.Service.dll"),
                                          new FileInfo("Microsoft.LiveFX.Client.dll"),
                                          new FileInfo("Microsoft.LiveFX.ResourceModel.dll"),
                                          new FileInfo("Microsoft.Web.dll"),
                                          new FileInfo("NLog.dll"),
                                      };
                foreach (var file in filesToCopy)
                    file.CopyTo(destination, true);
            }
            catch (IOException e)
            {
                throw new IOException("Unable to install service files due to a networking failure.", e);
            }
            catch(UnauthorizedAccessException e)
            {
                throw new UnauthorizedAccessException("Unable to install service files due to insufficient networking privileges.", e);
            }
        }

        public void StartService()
        {
            startService(ServiceController.GetServices());
        }

        public void StartService(string machineName)
        {
            startService(ServiceController.GetServices(machineName));
        }

        private static void startService(IEnumerable<ServiceController> services)
        {
            var service = services.SingleOrDefault(
                value => string.Equals(value.ServiceName, ServiceName, StringComparison.Ordinal));

            if (service == null)
                throw new InvalidOperationException("Service could not be started because it was not found.");

            service.Start();
            service.WaitForStatus(ServiceControllerStatus.Running, TimeSpan.FromSeconds(30));
        }

        public void StopService()
        {
            stopService(ServiceController.GetServices());
        }

        public void StopService(string machineName)
        {
            stopService(ServiceController.GetServices(machineName));
        }

        private static void stopService(IEnumerable<ServiceController> services)
        {
            var service = services.SingleOrDefault(
                value => string.Equals(value.ServiceName, ServiceName, StringComparison.Ordinal));

            // Service doesn't exist, and is thus already stopped.  Could throw an exception, but 
            if (service == null) return;
            if (service.Status != ServiceControllerStatus.Running) return;

            service.Stop();
            service.WaitForStatus(ServiceControllerStatus.Stopped, TimeSpan.FromSeconds(30));
        }


        public static bool IsInstalled()
        {
            return isInstalled(ServiceController.GetServices());
        }

        public static bool IsInstalled(string machineName)
        {
            return isInstalled(ServiceController.GetServices(machineName));
        }

        private static bool isInstalled(IEnumerable<ServiceController> services)
        {
            return services.Any(
                value => string.Equals(value.ServiceName, ServiceName, StringComparison.Ordinal));
        }
    }
}